/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package cbsnserverparte;

import java.net.*;
import java.io.*;
import java.util.*;

/**
 *
 * @author Craig Butler (5668670)
 * @author Stephen Norman (616000X)
 * @version 1.0 22/05/2008
 */
final class Protocol
{
    
    /* Private Variables */
        
    // Server Variables
    ServerSocket _serverSocket;
    
    // Connection to Relay
    private Socket _socket;
    
    // Reader Thread
    private Thread _readerThread;
    
    // Timer
    private Timer _dataCheckTmr;
    private Timer _timeoutTmr;
    
    // Reader
    private Reader _reader;
    
    /* Variables for GoBack-N */
    int _windowSize = 5;
    int _base = 1;
    int _nextSeqNum = 1;
    
    boolean timedOut = false;
    
    /* Constructors */
    
    /**
     * Constructor for our protocol class
     */
    Protocol()
    {        
        _dataCheckTmr = new Timer();
        _timeoutTmr = new Timer();
    }

    /**
     * Automated protocol used for showing GoBack-N.
     */
    public void commenceAutomatedActions()
    {
        // Try to establish a connection to the relay
        if (establishConnection() == false)
        {
            // We cannot continue as we cannot contact the relay
            System.out.println(Util.getTime() + "We could not contact the relay. Make sure it is running and then restart this application.");
            System.exit(1);
        }
        
        // We have an open connection to the relay and can proceed to the next phase
        // which is making sure we can communicate with the server and that it can communicate
        // with us.
        testReadAndWrite();
    }
    
    /**
     * Attempts to get a connection from the relay.
     * 
     * @return The success or failure of the connection.
     */
    private boolean establishConnection()
    {
        // Variable if the connection succeeds
        boolean _connectionSuccess;
        
        try
        {
            // Open a connection to the relay
            _serverSocket = new ServerSocket(4246);
            
            // Wait for a client to connect to use
            _socket = _serverSocket.accept();
            
            // Successful connection
            _connectionSuccess = true;
        }
        catch (Exception ex)
        {
            // Display the error to the user
            System.out.println(Util.getTime() + "Conection Error: " + ex.getMessage());
            
            // Connection failed
            _connectionSuccess = false;
        }
        
        // Return if we were successful or if we failed
        return _connectionSuccess;
    }
    
    /**
     * Since we now have our connection we should start sending
     * data immediately.
     */
    private void testReadAndWrite()
    {
        try
        {   
            // Create a new thread to read on
            DataInputStream ds = new DataInputStream(_socket.getInputStream());
            _reader = new Reader(ds);
            
            // Create the new thread
            _readerThread = new Thread(_reader);
            
            // Start the thread
            _readerThread.start();
            
            Writer.writer = new DataOutputStream(_socket.getOutputStream());
            
            // Finally we should go into the waiting phase
            _dataCheckTmr.schedule(new elapsedTime (), 2, 2);
            
            writePackets();            
            
            // Start the timeout timer
            _timeoutTmr = new Timer();
            _timeoutTmr.schedule(new timeoutExpired (), 800);
            
        }
        catch (Exception ex)
        {
            // We encounted a problem at phase 2
            System.out.println(Util.getTime() + "Phase 2 Exception: " + ex.getMessage());
            
            // Exit the application
            System.exit(1);
        }
    }
    
    private void writePackets()
    {
        try
        {
            while (_nextSeqNum < (_base + (_windowSize - 1)))
            {
                if (timedOut == true)
                    break;
                
                // Get the next chunk out of the fileData
//                byte[] dataToSend = getPacketChunk(fileData, 12, ((i - 1) * 12));
                
                // We should now be able to receive data from the relay
                String str = "Sample Message";
                Writer.writePacket(_nextSeqNum, str.getBytes());

                // Increment the next sequence number
                _nextSeqNum++;

                // Wait for 30 msecs
                Thread.sleep(30); 
            }
        }
        catch (Exception ex)
        {
            System.out.println(Util.getTime() + "Write Packet Error: " + ex.getMessage());
        }
    }
    
    class elapsedTime extends TimerTask
    {
        public void run()
        {
            checkForData();
        }
        
        private void checkForData()
        {
            try
            {
                // Check if there is any data available
                if (_reader.isDataAvailable() == true)
                {
                    processPacket(_reader.getData());
                }
            }
            catch (Exception ex)
            {
                System.out.println("Timer Error: " + ex.getMessage());
            }
        }
        
       /**
         * This is where we process the packet
         * that we have receieved
         * 
         * @param packet The incoming packet to process
         */
        private void processPacket(byte[] packet)
        {
            try       
            {
                /* Variables for incoming data */

                // Is the incoming data corrupt
                boolean isPacketCorrupt = false;

                // The sequence number of the packet
                int seqNum;

                // The MD5 hash of the packet's data
                byte[] hash;

                // The length of the data in the packet
                int dataLength;

                // The data in the packet
                byte[] data;

                // We need to extract the above variables 
                DataInputStream ds = new DataInputStream(new ByteArrayInputStream(packet));

                seqNum = ds.readInt();
                hash = new byte[32];
                ds.read(hash, 0, hash.length);
                dataLength = ds.readInt();
                data = new byte[dataLength];
                ds.read(data, 0, dataLength);       

                // Calculate the hash of the packet (now we have the data)
                String calcHash = Util.hashPacket(data);

                // Leave a blank line
                System.out.println();

                // Some dashes
                System.out.println("---------------------------------------------------");

                // Print out what is been sent
                System.out.println(Util.getTime() + "Receiving packet (contents below)");

                // Leave a blank line
                System.out.println();

                System.out.println("Sequence Number : " + seqNum);
                System.out.println("MD5 Hash        : " + new String(hash));
                System.out.println("Calculated Hash : " + calcHash);

                // Is the packet corrupt?
                if (calcHash.equals(new String(hash)))
                {
                    // The packet was not corrupt so tell the user
                    System.out.println("Packet Corrupt  : NO");
                }
                else
                {
                    // The packet was corrupt 
                    System.out.println("Packet Corrupt  : YES");
                    isPacketCorrupt = true;
                }

                // Display the data length (we can't show that data)
                System.out.println("Data Length     : " + data.length + " bytes");

                // Leave a blank line
                System.out.println();

                if (isPacketCorrupt == true)
                {
                    // If the packet was corrupt then it has been dropped
                    System.out.println(Util.getTime() + "Packet " + seqNum + " has been dropped due to corruption." );

                    // Leave a blank line
                    System.out.println();

                    // Stop processing this function
                    return;
                }
                
                if (seqNum < _base)
                {
                    // This may be out of order
                    System.out.println(Util.getTime() + "Packet " + seqNum + " has been dropped since we already have an ACK for a newer sequence number.");
                    
                    // Leave a blank line
                    System.out.println();
                    
                    // Stop processing
                    return;
                }                
                
                // Cancel the timer
                _timeoutTmr.cancel();
                
                // Make sure there is not timeout event running
                timedOut = false;
                
                // Process the packet, GoBack-N style
                _base = seqNum + 1;
                
                // Write the next set of packets
                writePackets();
                
                // Start the timeout timer (again)
                _timeoutTmr = new Timer();
                _timeoutTmr.schedule(new timeoutExpired (), 800);
                
            }
            catch (Exception ex)
            {
                System.out.println("Packet Read Error: " + ex.getMessage()); 
            }
        }
    }
    
     // Get a chunk of bytes from an array
   // Reference: http://www.java-tips.org/java-se-tips/java.io/reading-a-file-into-a-byte-array.html
   public static byte[] getPacketChunk(byte[] fileBytes, int length, int startPoint)
   {
       byte[] returnBytes = new byte[length];
       
       // Loop to get the chunk
       for (int i = 0; i < length; i++)
       {          
           returnBytes[i] = fileBytes[startPoint + i];
           
           if ((i + startPoint) == (fileBytes.length - 1))
               break;
       }
       
       return returnBytes;
   }
    
    // Used to return all the bytes in a file
    private byte[] getBytesFromFile(File file) throws IOException {
        InputStream is = new FileInputStream(file);
    
        // Get the size of the file
        long length = file.length();
    
        if (length > Integer.MAX_VALUE) {
            // File is too large
            
            System.out.println("Max Val: " + Integer.MAX_VALUE);
        }
    
        // Create the byte array to hold the data
        byte[] bytes = new byte[(int)length];
    
        // Read in the bytes
        int offset = 0;
        int numRead = 0;
        while (offset < bytes.length && (numRead=is.read(bytes, offset, bytes.length-offset)) >= 0)
        {
            offset += numRead;
        }
    
        // Ensure all the bytes have been read in
        if (offset < bytes.length) {
            throw new IOException("Could not completely read file "+file.getName());
        }
    
        // Close the input stream and return bytes
        is.close();
        return bytes;
    }

    
    class timeoutExpired extends TimerTask
    {
        public void run()
        {
                timedOut = true;
            
//              System.out.println("TIMEOUT EVENT 1");
            
                // Cancel the timer
                _timeoutTmr.cancel();
                
//               System.out.println("TIMEOUT EVENT 2");
                
                try
                {
                    int i = _base;
                    
                    while (i < _nextSeqNum)
                    {                        
                        // Get the bytes to send
                        //byte[] dataToSend = getPacketChunk(fileData, 12, i * 12);
                        
                        // We should now be able to receive data from the relay
                        String str = "Sample Message";
                        Writer.writePacket(i, str.getBytes());

                        // Increment the next sequence number
                        i++;

                        // Wait for 200 msecs
                        Thread.sleep(30); 
                    }
                }
                catch (Exception ex)
                {
                    System.out.println(Util.getTime() + "Write Packet Error: " + ex.getMessage());
                }
                
//                System.out.println("TIMEOUT EVENT 3");
                
                // Start the timeout timer
                _timeoutTmr = new Timer();
                _timeoutTmr.schedule(new timeoutExpired (), 800);
                
                timedOut = false;
                
//                System.out.println("TIMEOUT EVENT 4");
        }
    }
}
